package soapbot;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import javax.xml.soap.MimeHeaders;

import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.transport.http.NonBlockingBufferedInputStream;
import org.apache.axis.utils.Messages;
import org.apache.commons.logging.Log;


import groovy.lang.Binding;
import groovy.lang.GroovyShell;

public class HttpServer implements Runnable {

    static Log log = LogFactory.getLog(HttpServer.class.getName());

    private static final int BUFSIZ = 4096;
    // ASCII character mapping to lower case
    private static final byte[] toLower = new byte[256];  
    //HTTP Constants
    private static final byte getHeader[] = "GET".getBytes();
    private static final byte postHeader[] = "POST".getBytes();
    private static byte HTTP[] = "HTTP/1.0 ".getBytes();
    private static byte SEPARATOR[] = "\r\n\r\n".getBytes();
    // HTTP status codes
    private static byte OK[] = ("200 " + Messages.getMessage("ok00")).getBytes();
    private ServerSocket srvSocket;

    private Queue<MockResponse>  _reponseQueue = new LinkedList<MockResponse>();

    private static final String INVALID_REQUEST_RESPONSE = "";

    // becomes true when we want to quit
    private boolean done = false;


    public Queue<MockResponse>  getReponseQueue() {
        return _reponseQueue;
    }

    public void AddToReponseQueue(File responseFile) {
        _reponseQueue.add(new MockResponse(responseFile, null));
    }
    
    public void AddToReponseQueue(File responseFile, String groovyScript) {
        _reponseQueue.add(new MockResponse(responseFile, groovyScript));
    }

    public HttpServer(int port) {

        try {
            srvSocket = new ServerSocket(port);
        } catch (IOException ex) {
            log.error("Can't create server socket on port "+port, ex);
            System.exit(1);
        }

        Executor executor = Executors.newSingleThreadExecutor();
        executor.execute(this);      

        log.info("TCPListener is listening on port "+srvSocket.getLocalPort()+".");

    }

    public void run () {
        
        if (srvSocket == null) {
            return;
        }
        
        Socket sock;

        while (!done) {
            try {
                sock = srvSocket.accept();
                log.info("TCPListener received new connection: "+sock);
                new Thread(new SocketHandler(sock)).start();
            } catch (IOException ex) {

                log.debug("Got IOException on srvSocket.accept: "+ex);
            }
        }
    }


    public class SocketHandler implements Runnable {
        private Socket _socket;

        public SocketHandler (Socket socket) {
            this._socket = socket;
        }        


        public void run () {

            try {
                byte buf[] = new byte[BUFSIZ];

                // buffers for the headers we care about
                StringBuffer soapAction = new StringBuffer();
                StringBuffer httpRequest = new StringBuffer();
                StringBuffer fileName = new StringBuffer();
                StringBuffer cookie = new StringBuffer();
                StringBuffer cookie2 = new StringBuffer();
                StringBuffer authInfo = new StringBuffer();
                StringBuffer contentType = new StringBuffer();
                StringBuffer contentLocation = new StringBuffer();
                MimeHeaders requestHeaders = new MimeHeaders();


                NonBlockingBufferedInputStream is = new NonBlockingBufferedInputStream();
                InputStream ins = _socket.getInputStream();
                is.setInputStream(ins);

                int contentLength = parseHeaders(is, buf, contentType,
                        contentLocation, soapAction,
                        httpRequest, fileName,
                        cookie, cookie2, authInfo, requestHeaders);

                log.debug("contentLength : " + contentLength);

                is.setContentLength(contentLength);

                byte[] b = new byte[contentLength+2];
                is.read(b);

                MockResponse opt = _reponseQueue.poll();
                String request = new String(b);
                log.debug("Request : " + request);

                boolean isRequestValid = validateRequest(request, opt.getGroovyScript());
                log.debug("is request valid ? " + isRequestValid);                

                //wait before returning the response
                Thread.sleep(1000 * 2);
                
                log.debug("Wrting the response to socket");

                //OK now let's return the response
                OutputStream out = _socket.getOutputStream();
                out.write(HTTP);
                // assume the best
                out.write(OK);
                out.write(SEPARATOR);

                String response = null;
                if(isRequestValid) {
                    response = readFileAsString(opt.getResponseFile());
                } else {
                    response = INVALID_REQUEST_RESPONSE;
                }

                out.write(response.getBytes());

                out.write(SEPARATOR);

                out.flush();
                out.close();

            } catch (Exception e) {
                System.out.println(e.getMessage());
                e.printStackTrace();
            }

        }

        private boolean validateRequest(String request, String groovyScript) {
            if (groovyScript == null) {
                return true;
            }
            Binding binding = new Binding();
            binding.setVariable("request", request);
            GroovyShell shell = new GroovyShell(binding);       
            return (Boolean) shell.evaluate(groovyScript);  
        }        

        private String readFileAsString(File file) throws java.io.IOException
        {
            StringBuffer fileData = new StringBuffer(1000);
            BufferedReader reader = new BufferedReader(new FileReader(file));
            char[] buf = new char[1024];
            int numRead = 0;
            while ((numRead = reader.read(buf)) != -1) {
                String readData = String.valueOf(buf, 0, numRead);
                fileData.append(readData);
                buf = new char[1024];
            }
            reader.close();
            return fileData.toString();
        }        
        private int parseHeaders(NonBlockingBufferedInputStream is, byte buf[], StringBuffer contentType,
                StringBuffer contentLocation, StringBuffer soapAction, StringBuffer httpRequest, StringBuffer fileName,
                StringBuffer cookie, StringBuffer cookie2, StringBuffer authInfo, MimeHeaders headers)
        throws java.io.IOException {

            int n;
            int len = 0;

            // parse first line as GET or POST
            n = this.readLine(is, buf, 0, buf.length);
            if (n < 0) {
                // Because sometimes shit happens
                throw new java.io.IOException(Messages.getMessage("unexpected EOS"));
            }

            // which does it begin with?
            httpRequest.delete(0, httpRequest.length());
            fileName.delete(0, fileName.length());
            contentType.delete(0, contentType.length());
            contentLocation.delete(0, contentLocation.length());

            if (buf[0] == getHeader[0]) {
                httpRequest.append("GET");
                for (int i = 0; i < n - 5; i++) {
                    char c = (char) (buf[i + 5] & 0x7f);
                    if (c == ' ')
                        break;
                    fileName.append(c);
                }
                log.debug(Messages.getMessage("filename01", "SimpleAxisServer", fileName.toString()));
                return 0;
            } else if (buf[0] == postHeader[0]) {
                httpRequest.append("POST");
                for (int i = 0; i < n - 6; i++) {
                    char c = (char) (buf[i + 6] & 0x7f);
                    if (c == ' ')
                        break;
                    fileName.append(c);
                }
                log.debug(Messages.getMessage("filename01", "SimpleAxisServer", fileName.toString()));
            } else {
                throw new java.io.IOException(Messages.getMessage("badRequest"));
            }

            while ((n = readLine(is, buf, 0, buf.length)) > 0) {

                if ((n <= 2) && (buf[0] == '\n' || buf[0] == '\r') && (len > 0)) break;

                //JAVA 6
                //String line = new String(Arrays.copyOf(buf, n));
                //JAVA 5 START
                byte[] b = new byte[n];
                for (int i = 0; i < b.length; i++) {

                    if(i<buf.length) {
                        b[i] = buf[i];
                    }else{
                        b[i] = (byte)0;
                    }

                }
                String line = new String(b);
                //JAVA 5 END

                if(line.startsWith("Content-Length:")) {
                    return Integer.parseInt(line.substring(line.indexOf(':')+1).trim());
                }

            }
            return len;

        }

        /**
         * Read a single line from the input stream
         * @param is inputstream to read from
         * @param b byte array to read into
         * @param off starting offset into the byte array
         * @param len maximum number of bytes to read
         */
        private int readLine(NonBlockingBufferedInputStream is, byte[] b, int off, int len)
        throws java.io.IOException {
            int count = 0, c;

            while ((c = is.read()) != -1) {
                if (c != '\n' && c != '\r') {
                    b[off++] = (byte) c;
                    count++;
                }
                if (count == len) break;
                if ('\n' == c) {
                    int peek = is.peek(); //If the next line begins with tab or space then this is a continuation.
                    if (peek != ' ' && peek != '\t') break;
                }
            }
            return count > 0 ? count : -1;
        }          
    }


    /**
     * does tolower[buf] match the target byte array, up to the target's length?
     */
    public boolean matches(byte[] buf, byte[] target) {
        for (int i = 0; i < target.length; i++) {
            if (toLower[buf[i]] != target[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Case-insensitive match of a target byte [] to a source byte [],
     * starting from a particular offset into the source.
     */
    public boolean matches(byte[] buf, int bufIdx, byte[] target) {
        for (int i = 0; i < target.length; i++) {
            if (toLower[buf[bufIdx + i]] != target[i]) {
                return false;
            }
        }
        return true;
    }
}


